Skip navigation and go to content

Cypress Integration Tests for Client-Side Routing

On this page

Like many web applications, CampSpots takes a client-side routing approach.

When an app uses client-side routing, it takes control of browser navigation away from the browser.

We need to make sure that someone who uses a keyboard or a screen reader has as good of an experience as a sighted mouse user does.

When a navigation link is clicked, the page should update accessibly. With traditional HTML navigation, the page refreshes and focus is reset to the top. There is also a configurable screen reader summary that reads out the page content.

For client-side routing with JavaScript, this functionality needs to be replaced somehow so users aren’t left wondering if anything happened after clicking a link. Keyboard focus should move somewhere, the page change should be announced, and the title should change.

In this section we’ll write some tests around routing to make sure we aren’t breaking anything by using React. Along the way, we’ll make decisions and talk about the trade-offs that come with client-side routing.

In terms of implementation, CampSpots uses Reach Router, from the same team behind React Router and Remix.

Reach Router is recommended for applications with only a few routes, and React Router for everything else. Ryan Florence, co-author of both React routing libraries, wrote a blog post explaining more.

💡Tip

The concepts and testing approaches in these lessons are applicable no matter which solution you use!

🛠 Challenge: Preparing the Routing Tests

Inside of the exercise3 directory is an empty Routing.test.js file that needs to be prepared to test client side routing in CampSpots.

Your challenge is to write the starter for two tests: The first will test that the title changes when navigating to a new page.

The second test will test that the user is able to navigate to a new page using the keyboard.

Both of these tests will use the CampSpots homepage as its starting point.

Don’t worry about filling in the test logic yet— this is just creating the skeleton for the test file!

🛠 Solution: Preparing the Routing Tests

Here’s what the starting point of the Routing tests should look like:

describe('Routing', () => {
	beforeEach(() => {
		cy.visit('http://localhost:1234')
	})

  it('should have title changes', () => {

  })
  
  it('should change the page via keyboard', () => {
  
  })
})
Video: Preparing the Routing Tests
Loaded: 4%
Current Time 0:00
/
Duration Time 1:55
Video Transcript

I'm going to get started with a new file for our routing, and I'm gonna make it called routing dot spec dot JS. We are going to describe, and I'm going to just say routing initially I was calling it client side routing, but it's really just routing. So that means going from page to page, you know, making sure that our application is navigable from a screen reader and a keyboard.

So before each. I am going to C dot visit 0 0 0 0 1 2 3 4 slash workshop two automated testing. And that plops us into the, the right part of this web application. So I'm going to make sure that this loads in the right place by saying C do get header. And that's a cool thing about Cyprus is that you can use those get commands, just to make sure that it's in the right space.

It's a, a command to get something and then do things. But it, you can also kind of use it to assert in its own, right. Just because it'll fail if it doesn't work. So with this. And we're gonna have to add some code to make this work, but it should have title changes. So as we use, we're using reach router in this application, which you can see in the app JS file.

There is a router here that is creating these pages on the client side and it uses reach router. So when we change those pages, We should update the page title. So that way there is something announced in a screen reader when the page changes. So we're gonna go in and do that. It also, this one we're gonna, we're gonna talk about this one.

This one might be hard to do so it should ideally it. If we're doing client side routing, it should move. Focus on a user initiated page change. This, we will decide whether we, how far we want to go to make this one, work it at the very least should have title changes. And I'll tell you why.

Note: This video reflects an older project structure

Testing Page Changes

If you check out the index.html file in the repo, you’ll find that the <title> tag has been hardcoded to “CampSpots”.

Because we are working in a React application, we don’t have access to change stuff in the document head directly. All of the page-level components that we build will have the same hardcoded title.

Fortunately, there’s a library called React Helmet that allows us to manipulate things in the document head.

React Helmet is already installed as a dependency in the workshop repo and has been imported at the top of the page components:

// inside of page-home.js
import {Helmet} from 'react-helmet'

Dynamically Set the Home Page Title

Inside of the HomePage component at page-home.js, we can add the <Helmet> component. The children inside of the component will look exactly the same as they would if we were writing the document head in plain HTML. Any tags we add will override what’s currently present in index.html.

Here’s an example of adding Helmet with the HTML title tag to the homepage:

// inside page-home.js
const HomePage = () => {
	return (
		<BodyClassName className="page-home">
			<>
				<HeaderPortal>
          <h1 className="visually-hidden">Camp Spots</h1>
          <Helmet>
             <title>Testing</title>
          </Helmet>
        </HeaderPortal>
		...

The title change is then reflected in the browser:

The “Testing” title shows for the homepage.

Now that we know Helmet’s title update works, we can switch the title to say “CampSpots” for the homepage.

📝Exercise

Add React Helmet to Other Pages

As an exercise, use React Helmet to add titles to the other CampSpots pages. Fix any missing HeaderPortals and H1s while you’re at it!

Video: Use React Helmet to Dynamically Set Page Titles
Loaded: 6%
Current Time 0:00
/
Duration Time 1:23
Video Transcript

Because this is a react, react application. It's not going to change the title unless we do something special. So I'm gonna get us started in using the react helmet library. So I already have it in each of the page components and I can go into the component and say helmet. So helmet is essentially like we.

Kind of drill down in our component tree into the page home component, which is a component for a page. But what we wanna do is inject a title up into the head of the document. So react helmet gives us that access. So I can say title as if it were going into the head. I can say camp spots. So for the homepage, it will add that title dynamically of camp spots.

And that will override the default, which is in our index HTML file. So this says camp spots, but I want it to be more specific when I go from page to page. So it should update. So that's really what we're doing with react helmet. And when I go into page about. I'm gonna do the same thing, but I'm gonna change the name.

So I need helmet and then title. And I will say about camp spots. And then one more page listings. I will do the same, cuz these are our three pages that we're using right now. And I will say title listings, camp spots.

Note: This video reflects an older project structure

🛠 Challenge: Write a Test that Asserts the Title Changes

Write a test that asserts the title changes when visiting the About page on the CampSpots site.

Since the beforeEach is set up to start on the homepage, check that the title matches what you set in page-home.js.

Navigate to the About page, and assert that the page title has changed.

Follow the TDD approach– write a test that fails, then update your markup to make it pass.

Hint: If you get stuck, this Title syntax page in the Cypress API Docs will help!

🛠 Solution: Title Change Test

Here’s what my title change test looks like:

it('should have title changes', () => {
		// This test requires changes with React Helmet
    cy.title().should('eq', 'Camp Spots')

    cy.visit('http://localhost:1234/about')

    // test for title change
    cy.title().should('eq', 'About Camp Spots')
})

Thoughts on Client-Side Routing

The CampSpots app uses Reach Router which manages focus for you if you use its <Link> component.

But you may find that sometimes navigating around the app will jump focus part way down the page.

Honestly, client-side routing is hard to get right.

After doing my own testing and trying to make it work the way I wanted, I opted to not fully rely on client-side routing in this application.

One of the state issues I found when initially developing the CampSpots demo app was that using links from within the MegaNav would navigate the page but not close the menu. This alone wouldn’t be too difficult to fix but when combined with the focus jumping an undesirable place, client-side routing didn’t really feel worth it.

I’m still using Reach Router to render the pages but the navigation uses regular anchor tags instead of <Link> components.

If I were to ship CampSpots to production, I would also make it use server-rendered HTML instead of relying on client-side JavaScript to route and render pages.

That said, there definitely are some types of applications where using client-side routing can provide a better experience. If your app needs to maintain a lot of state without refreshing or deep links navigation to subviews, it probably would be a good candidate. But you’ll have to manage screen reader announcements and move focus yourself.

💡Tip

The Coding Accessible Interactions & Mechanics workshop has more on announcements and focus management.

Video: Client-Side Routing Thoughts
Loaded: 1%
Current Time 0:00
/
Duration Time 5:37
Video Transcript

So I promised that we would talk a bit about client side routing, and it's been interesting working on this application to test it, to, you know, see what's happening in this application. So as I showed you, we are using reach router. It is a tool that handles focus management for you. If you saw earlier, when I was clicking through the page, however, it was jumping focus part of the way down the page on the homepage, because that's.

guess where it thinks it needs to focus, which is not ideal. And honestly, it's like, it's a pain or client side routing. It just is not easy to get. Right. I guess, using frameworks. And so. In this web application, I have opted. And after trying to test it and trying to use the tools available, I've decided to not use the client side routing part of this.

So if I were to ship this application to production, I would really want to make sure I have server rendered pages, cuz I do love the modern tooling of react in this case. To let me do ING and component driven development, do all this automated testing, but if I could have it generate server rendered HTML for me, that would be awesome.

And then I can just use regular anchor links. So I'm still using the client side router to render the pages. But they have regular URLs. So the URLs being things like slash listings, so it doesn't have a hashed URL, you know, using the hash symbol, the number symbol. So I am going to opt out of using the reach router link component and just use anchor elements, cuz it seems to work better.

So. That impacts a few different things. It means that we can't, I'm not gonna write a test that asserts focus in Cyprus, but I can write something to make sure that we can navigate by keyboard to change the page. So we, we will write a test for that instead.

So we're testing that we can reach this mega menu we can navigate, and that it goes one step further to change the page. So kind of a page level concern for keyboard accessibility. And that's because I really just don't wanna use client side routing for what it gives us. The tooling makes it a lot harder than it needs to be.

So just let the page refresh if you can. That refreshes the, the focus points. So users with keyboards and screen readers aren't left wondering whether anything happened. It will also announce a page change announcement because we're using react helmet to change the page title. And I found it was just, it worked better, honestly, trying to.

Trying to like craft the user's focus through at least in this application. I just didn't need all that complexity. Now I should say that there are some web applications where you really, it might be a better experience to use client side routing. And, you know, if there's some kind of a web application that needs more transitions or it needs to maintain state without refreshing the page, those kinds of things, that is an area where you get back into client side, routing.

Where JavaScript is handling the browser history and you do need to handle focus, you know, move, focus into the new content. And so I have talked about that in previous workshops, but the routing situation right now is unfortunate. So until we get better tools, I'm just gonna say, if you can let the page refresh in between do it.

it's so much easier. So let's do that. We're gonna, we're gonna go with that today.

So there are a few places where if you want to play with re TRR, you can our header. So our mega menu, there's a couple places, the header for one. So here's a spot where re router's link component works. I'm gonna change this back to an anchor. It's more predictable. And I tend to agree with the react remix, I guess, react training re remix is their new thing, react training.

They're the team that created reach router and react router. I tend to agree with them that the way that reach router was trying to move focus automatically just didn't work. It didn't work in this website, you know, as I was building this, it didn't give me enough control. So I'm really pushing router developers, you know, library authors to give us more APIs so that we can do, we can have more control over it.

That would be nice, but we don't have those tools yet. So I'm gonna change back to a regular link. So that's on the header logo. We did that one. So just getting the client side, routing part out of it, we're using the routing, the rendering part, and that works fine, but in our links as well. So we already have regular links in our, in the list of links.

Let me show you again to refresh your memory. So these top toggles for the mega menu are buttons because they don't navigate the user. They. Open and close the mega menu. And then these links in these little link groups, I called them. Those are the ones that we're really we wanna talk about. I'm gonna let them refresh.

And the reason is that if we try and use a router, It gets complicated fast. First of all, with re TRR, it did not reset the state to close the mega menu. So it was navigating, but I had to close the mega menu and I could figure that part out, but then moving the focus into a different place. It just, the automatic behavior that it had was not working.

So. Let the page refresh, it's just simpler. so much easier. So let's just go with it. If you can just do it for all the promise that we have with client side routing, it makes our lives way more complicated than they need to be.

🛠 Challenge: Test Page Changes via Keyboard

Write a Cypress test that simulates using the keyboard to activate a link in the MegaNav. Assert that the page has changed.

🛠 Solution: Test Page Changes via Keyboard

There are multiple ways to do this but here’s my solution.

Because of the beforeEach block, I know that Cypress will navigate to the homepage successfully.

The first thing I’ll do is use cy.get() and pass in the selector for the first toggle button in the MegaNav. Then I’ll chain on calls to .focus() and .click() to open the first submenu.

Remember from earlier that this has the added benefit of ensuring that the MegaNav buttons are still focusable. Calling .click() has the same effect as hitting the Enter key once the button has focus.

With the menu open, I’ll use cy.realPress() and simulate hitting the Tab key to traverse the submenu. The first link in the menu will have received focus from the Tab key press, so I can call cy.focused().click() to click it and navigate to the Listings page.

💡Tip

If I was using the full client-side routing approach, now is when I would write an assertion about what element should receive focus after navigation is complete.

In order to check if the page has changed, I’ll check that React Helmet has set the title to contain “Listings” like we set up earlier.

Here’s why my test looks like:

it('should change the page via keyboard', () => {
  // open meganav
  cy.get('[data-testid="megamenu-section1"]').focus().click()

  // tab into nav
  cy.realPress('Tab')

  // click first nav item for /listings page
  cy.focused().click()

  cy.title().should('contain', 'Listings')
})

When I run the test in Cypress, it passes as expected:

Cypress shows the page change test passes
Video: Start Page Change Test
Loaded: 20%
Current Time 0:00
/
Duration Time 3:19
Video Transcript

So let's write a test that asserts that we can navigate into the mega menu, operate it via keyboard, and that the page will change. So let's change it from the homepage. So C do get, and I will do button data, test ID using that attribute selector mega menu, section one. I need to close my quotes and my data attribute square brackets.

I'm going to focus on that. Make sure that's still focusable, which we're doing kind of a two parter with button in the selector and the focus. And I'm doing it this way with the focus ahead of the click, since Cyprus does not give me those real events that we talked about earlier. So trying to fire a keyboard event at it, you could try it with the Cypress real press.

But yeah, the, the best practice I've found is just send a focus event to it. And if it's not focusable, it will. That is reliable. So I am gonna say side dot real press use that Cypress real event library. So that's, we're focused on the first mega menu item, like button. We're gonna focus on it. Hit click to be kind of like an enter almost.

And then when we hit tab, the menu should be open. We could even put a comment. Mega menu. Section one should be open. I could even write a test for that, but that's not exactly what we're testing. So I'm going to say side up focused click. So that should be that first focusable item within the menu. It's the open mega menu section one.

And that will take us to the listings page. So when I was thinking of writing this test for client side routing, from this point, if I was doing client side routing, I could assert what element would get automatically focused, but I'm not gonna do that. I'm gonna let it refresh the page. And I'll show that to you.

I could even fire up voiceover and show you how that works. Cuz react Hillman actually does a really good job of updating the page title. And so. With that and kind of letting the focus, just be reset, like a traditional page reload. I think it works pretty well, certainly better than just leaving the focus where it was in a client side routing environment.

That's no good. So we changed the page. So side title should EQ listings, camp spots. So it could EQ maybe like, I don't know if there's a should includes API and Cypress or something, but that could be helpful. So listings, camp spots, let's go see. It should change the page. Yeah. That worked. So I can go back through this part, which is pretty cool.

And it changed the page. Sweet. So I made sure that item was focused. I made sure it navigated and that works awesome. I guess one thing we could do to make this test. More, I don't know. I'm thinking about alternative ways to write it, cuz this is very dependent on the first item in the nav being the listings link.

So maybe I could like get the URL off of the link and then see if it navigates to that page. I don't know what value that would provide. I'm just thinking of ways to make the test more. Solid and more programmatic, not like hard coded because what we don't wanna do is like if the marketing team wants to rearrange the nav items and we have tests that are like very tightly coupled with the order of the nav items, that might be kind of a brittle test, but we asserted that that behavior worked.

So it works. Okay.